package pt.compflow.graph;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;

import pt.compflow.flowontology.model.FLOWO;
import pt.ipp.isep.gecad.onto2flow.model.Edge;
import pt.ipp.isep.gecad.onto2flow.model.Graph;
import pt.ipp.isep.gecad.onto2flow.model.Node;
import pt.ipp.isep.gecad.onto2flow.model.O2F;
import pt.ipp.isep.gecad.onto2flow.model.Onto2FlowModel;
import pt.ipp.isep.gecad.onto2flow.model.Onto2FlowModelFactory;

import com.hp.hpl.jena.query.Query;
import com.hp.hpl.jena.query.QueryExecution;
import com.hp.hpl.jena.query.QueryExecutionFactory;
import com.hp.hpl.jena.query.QueryFactory;
import com.hp.hpl.jena.query.ResultSet;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.impl.ResourceImpl;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;

public class GraphWrapper {
			
	protected Onto2FlowModel model;
	protected Graph graph;
	
	private String getNodeUri(Resource res) {
		if(res.isURIResource() && !res.isLiteral()) {
			return res.getURI() + "-" + graph.getLocalName();
		}
		
		return getUniqueId(graph.getLocalName() + "-n", model);
	}
	
	public GraphWrapper() {
		this.model = Onto2FlowModelFactory.createDefaultModel();
		this.graph = model.createGraph(getUniqueId("g", model));
	}
	
	public GraphWrapper(String graphUri) {
		this.model = Onto2FlowModelFactory.createDefaultModel();
		if(graphUri == null) {
			this.graph = model.createGraph(getUniqueId("g", model));
		} else {
			this.graph = model.createGraphWithURI(graphUri);
		}
	}
	
	public GraphWrapper(Onto2FlowModel model) {
		this.model = model;
		this.graph = model.createGraph(getUniqueId("g", model));
	}
	
	public GraphWrapper(Onto2FlowModel model, String graphUri) {
		this.model = model;
		if(graphUri == null) {
			this.graph = model.createGraph(getUniqueId("g", model));
		} else {
			this.graph = model.getGraphWithURI(graphUri);
			if(this.graph == null) this.graph = model.createGraphWithURI(graphUri);
		}
	}
			
	public void addPath(Path p) {
		if(p == null) throw new IllegalArgumentException("Path is null");
		for(Edge e : p) 
			addEdge(e.getFromNodeNode().getElement(), e.getProperty(), e.getToNodeNode().getElement());
	}
	
	public Node addNode(Resource res) {
		Node n = model.getNode(getNodeUri(res));
		if(n == null) {
			n = model.createNodeWithURI(getNodeUri(res));
			n.setElement(res);
		}
		graph.addNodeNode(n);
		return n;
	}
	
	public Node getNode(Resource res) {
		Node n = model.getNodeWithURI(getNodeUri(res));
		if(n == null) return null;
		if(!graph.hasProperty(O2F.node, n)) return null;
		return n;
	}
	
	public ExtendedIterator<Node> listNodes() {
		return graph.listNodeNodes();
	}
	
	public List<Node> getNodes() {
		return graph.listNodeNodes().toList();
	}
	
	public Node removeNode(Resource res) {
		Node n = getNode(res);
		if(n == null) return null;
		
		Iterator<Edge> eIt = n.listOutEdgeEdges();
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			model.removeAll(e, null, null);
			model.removeAll(null, null, e);
		}
		
		eIt = n.listInEdgeEdges();
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			model.removeAll(e, null, null);
			model.removeAll(null, null, e);
		}
		
		model.removeAll(null, null, n);
		model.removeAll(n, null, null);
		
		return n;
	}
	
	public void remove() {
		for(Node n : getNodes()) {
			removeNode(n.getElement());
		}
		
		model.removeAll(graph, null, null);
		model.removeAll(null, null, graph);
	}
			
	public Edge addEdge(Resource fromRes, Resource propRes, Resource toRes) {
		Node from = addNode(fromRes);
		Node to = addNode(toRes);
				
		if(getEdge(fromRes, propRes, toRes) != null) return null;
		
		Edge ne = model.createEdge(getUniqueId("e" + graph.getLocalName(), model));
		ne.setProperty(propRes);
		ne.setToNodeNode(to);
		ne.setFromNodeNode(from);
				
		// FIXME for now avoid inEdge if nodes are the same
		// allows detecting "start" nodes in the graph (those without inedges)
		if(!to.equals(from)) to.addInEdgeEdge(ne);
		from.addOutEdgeEdge(ne);
		
		graph.addEdgeEdge(ne);
			
		return ne;
	}
	
	public Edge getEdge(Resource fromRes, Resource propRes, Resource toRes) {
		QueryExecution qe = listEdges(fromRes, propRes, toRes);
		ResultSet eIt = qe.execSelect();
		if(eIt == null || !eIt.hasNext()) {
			qe.close();
			return null;
		}
		Edge e = eIt.nextSolution().getResource("e").as(Edge.class);
		qe.close();
		return e;
	}
	
	public QueryExecution listEdges(Resource withRes) {
		return listEdgesBi(withRes, null, null);
	}
	
	public QueryExecution listEdgesBi(Resource fromRes, Resource propRes, Resource toRes) {
		
		String fromRest = fromRes == null ? null : "?e <" + O2F.fromNode + "> <" + getNodeUri(fromRes) + "> .";
		String propertyRest = propRes == null ? null : "?e <" + O2F.property + "> <" + propRes + "> .";
		String toRest = toRes == null ? null : "?e <" + O2F.toNode + "> <" + getNodeUri(toRes) + "> .";
		
		String fromRest2 = fromRes == null ? null : "?e <" + O2F.toNode + "> <" + getNodeUri(fromRes) + "> .";
		String toRest2 = toRes == null ? null : "?e <" + O2F.fromNode + "> <" + getNodeUri(toRes) + "> .";
		
		String queryString = "SELECT DISTINCT ?e WHERE { <" + graph.getURI() + "> <" + O2F.edge.getURI() + "> ?e . { " + (fromRest != null ? fromRest : "") + (propertyRest != null ? propertyRest : "") + (toRest != null ? toRest : "") + 
				" } UNION { " + (fromRest2 != null ? fromRest2 : "") + (propertyRest != null ? propertyRest : "") + (toRest2 != null ? toRest2 : "") + " } }";
		
		Query query = QueryFactory.create(queryString);
		QueryExecution qexec = QueryExecutionFactory.create(query, model);
		
		return qexec;
	}
		
	public QueryExecution listEdges(Resource fromRes, Resource propRes, Resource toRes) {
		
		String fromRest = fromRes == null ? null : "?e <" + O2F.fromNode + "> <" + getNodeUri(fromRes) + "> .";
		String propertyRest = propRes == null ? null : "?e <" + O2F.property + "> <" + propRes + "> .";
		String toRest = toRes == null ? null : "?e <" + O2F.toNode + "> <" + getNodeUri(toRes) + "> .";
		
		String queryString = "SELECT ?e WHERE { <" + graph.getURI() + "> <" + O2F.edge.getURI() + "> ?e . " + (fromRest != null ? fromRest : "") + (propertyRest != null ? propertyRest : "") + (toRest != null ? toRest : "") + " }";
		Query query = QueryFactory.create(queryString);
		QueryExecution qexec = QueryExecutionFactory.create(query, model);
		
		return qexec;
	}
	
	public Edge removeEdge(Resource fromRes, Resource propRes, Resource toRes) {
		Edge e = getEdge(fromRes, propRes, toRes);
		if(e == null) return null;
		
		model.removeAll(e, null, null);
		model.removeAll(null, null, e);
			
		return e;
	}
			
	public List<Path> getAllPathsBetweenNonDirectional(Resource r1, Resource r2, EdgeFilter f, int maxLength) {
		
		Node c1 = getNode(r1);
		Node c2 = getNode(r2);
		
		if(c1 == null || c2 == null)
			throw new NullPointerException("At least one of the supplied nodes does not exist in the graph.");
		
		List<Path> paths = new ArrayList<Path>();
		
		Iterator<Edge> eIt = c1.listOutEdgeEdges();
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(f==null || f.accept(null, e)) {
				Path p = new Path();
				p.add(e);
				getAllPathsBetweenRec(e.getToNodeNode(), c2, p, paths, f, maxLength, true);
			}
		}
		
		eIt = c1.listInEdgeEdges();
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(f==null || f.accept(null, e)) {
				Path p = new Path();
				p.add(e);
				getAllPathsBetweenRec(e.getFromNodeNode(), c2, p, paths, f, maxLength, true);
			}
		}
		
		return paths;
	}
		
	public List<Path> getAllPathsBetween(Resource r1, Resource r2, EdgeFilter f, int maxLength) {
		
		Node c1 = getNode(r1);
		Node c2 = getNode(r2);
		
		if(c1 == null || c2 == null)
			throw new NullPointerException("At least one of the supplied nodes does not exist in the graph.");
		
		List<Path> paths = new ArrayList<Path>();
		
		Iterator<Edge> eIt = c1.listOutEdgeEdges().toList().iterator();  //FIXME another concurrent exception...
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(f==null || f.accept(null, e)) {
				Path p = new Path();
				p.add(e);
				getAllPathsBetweenRec(e.getToNodeNode(), c2, p, paths, f, maxLength, false);
			}
		}
		
		return paths;
	}
	
	private void getAllPathsBetweenRec(Node to, Node n, Path currentPath, List<Path> paths, EdgeFilter f, int maxLength, boolean nonDirectional) {
		
		if(maxLength > 0 && currentPath.size() > maxLength) return;
				
		if(to.equals(n)) {
			paths.add(currentPath);
			//return; //allow cycles
		}
		
		Iterator<Edge> eIt = to.listOutEdgeEdges().toList().iterator(); //FIXME another concurrent  exception...
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(!currentPath.contains(e) && (f==null || f.accept(currentPath, e))) {
				Path path = new Path(currentPath);
				path.add(e);
				getAllPathsBetweenRec(e.getToNodeNode(), n, path, paths, f, maxLength, nonDirectional);
			}
			
		}
		
		if(nonDirectional) {
			eIt = to.listInEdgeEdges().toList().iterator(); //FIXME another concurrent exception...
			while(eIt.hasNext()) {
				Edge e = eIt.next();
				if(!currentPath.contains(e) && (f==null || f.accept(currentPath, e))) {
					Path path = new Path(currentPath);
					path.add(e);
					getAllPathsBetweenRec(e.getFromNodeNode(), n, path, paths, f, maxLength, nonDirectional);
				}	
			}
		}
	}
	
	private void applyTransitiveReduction(Resource propRes, Path path, Node to, List<Edge> edgesToRemove) {
				
//		// check if path is still valid (may be cut out from other recursive invocation)
//		for(int i=0; i<path.size(); i++) {
//			Edge<T, E> edge = path.get(i);
//			if(getEdge(edge.getFrom().getNode(), edge.getProperty(), edge.getTo().getNode()) == null)
//				return;
//		}
		
		Edge lastEdge = path.get(path.size()-1);
		for(int i=0; i<path.size()-1; i++) {
			Edge edge = path.get(i);
			Edge redEdge = getEdge(edge.getFromNodeNode().getElement(), lastEdge.getProperty(), to.getElement());
			if(redEdge != null) edgesToRemove.add(redEdge);				
		}
		
		// just continue to traverse the graph
		Iterator<Edge> eIt = to.listOutEdgeEdges().toList().iterator(); // FIXME for some reason there's a ConcurrentModificationException here...
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(!path.contains(e) && (propRes == null || propRes.equals(e.getProperty()))) {
				Path p = new Path(path);
				p.add(e);
				applyTransitiveReduction(propRes, p, e.getToNodeNode(), edgesToRemove);
			}	
		}
	}
	
	public void applyTransitiveReduction(Resource propRes) {
		List<Edge> edgesToRemove = new ArrayList<Edge>();
		
		Iterator<Node> nIt = graph.listNodeNodes();
		while(nIt.hasNext()) {
			Node node = nIt.next();
			if(node.getInEdgeAmount() == 0) {
				Iterator<Edge> eIt = node.listOutEdgeEdges().toList().iterator(); // FIXME for some reason there's a ConcurrentModificationException here...
				while(eIt.hasNext()) {
					Edge e = eIt.next();
					if(propRes == null || propRes.equals(e.getProperty())) {
						Path p = new Path();
						p.add(e);
						applyTransitiveReduction(propRes, p, e.getToNodeNode(), edgesToRemove);
					}	
				}
			}
		}
		
		for(Edge redEdge : edgesToRemove)
			if(redEdge.getFromNodeNode() != null && redEdge.getToNodeNode() != null)
				removeEdge(redEdge.getFromNodeNode().getElement(), redEdge.getProperty(), redEdge.getToNodeNode().getElement());
	}
	
	public GraphWrapper diff(GraphWrapper g, Onto2FlowModel model) {
		GraphWrapper diff = new GraphWrapper(model);
		
		Iterator<Edge> eIt = graph.listEdgeEdges();
		while(eIt.hasNext()) {
			Edge e = eIt.next();
			if(g.getEdge(e.getFromNodeNode(), e.getProperty(), e.getToNodeNode()) == null)
				diff.addEdge(e.getFromNodeNode(), e.getProperty(), e.getToNodeNode());
		}
		
		return diff;
	}
	
	public void assimilate(GraphWrapper g) {
		QueryExecution qe = g.listEdges(null, null, null);
		ResultSet eIt = qe.execSelect();
		while(eIt.hasNext()) {
			Edge e = eIt.nextSolution().getResource("e").as(Edge.class);
			addEdge(e.getFromNodeNode().getElement(), e.getProperty(), e.getToNodeNode().getElement());
		}
		qe.close();
	}
			
	public Onto2FlowModel getModel() {
		return model;
	}

	public void setModel(Onto2FlowModel model) {
		this.model = model;
	}

	public Graph getGraph() {
		return graph;
	}

	public void setGraph(Graph graph) {
		this.graph = graph;
	}
	
	public String getUniqueId(String id, Model model) {
		UUID uuid = UUID.randomUUID();
		
		while(model.containsResource(new ResourceImpl(FLOWO.getURI(), id + "-" + uuid)))
			uuid = UUID.randomUUID();
			
		return id + "-" + uuid;
	}
	
	private void buildPrint(StringBuilder builder, Iterator<Edge> edges, int level, List<Node> path) {
		if(edges == null) return;
		
		while(edges.hasNext()) {
			Edge e = edges.next();
			
			Node n1 = e.getToNodeNode();
			
			for(int i=0;i<level+1;i++) builder.append("\t");
			builder.append("-- (");
			builder.append(e.getProperty().getLocalName());
			builder.append(") --> ");
			builder.append((n1.getElement().getLocalName().trim().length() == 0 ? n1.getElement().getURI() : n1.getElement().getLocalName()) + "\n");
		
			if(!path.contains(n1)) {
				List<Node> npath = new ArrayList<Node>(path);
				npath.add(n1);
				buildPrint(builder, n1.listOutEdgeEdges().toList().iterator(), level+2, npath);
			}
		}
	}
	
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();

		Iterator<Node> nIt = graph.listNodeNodes();
		while(nIt.hasNext()) {
			Node node = nIt.next();
			if(node.getInEdgeAmount() <= 0) { 
				List<Node> path = new ArrayList<Node>();
				path.add(node);
				builder.append(node.getElement().getLocalName() + "\n");
				buildPrint(builder, node.listOutEdgeEdges().toList().iterator(), 0, path);
			}
		}
		
		return builder.toString();
	}

}
